package com.depth.viewer.utils.json
{
	
	/**
	 * @example ActionScript to use JSONTokenizer: 
	 * <listing version="3.0">
	 * var s:String;
	 * var jSONTokenizer:JSONTokenizer = new JSONTokenizer(s);
	 * </listing>
	 *
	 **/
	
	public class JSONTokenizer extends Object
	{
		private var obj:Object;
		
		private var jsonString:String;
		
		private var loc:int;
		
		private var ch:String;
		
		public function JSONTokenizer( s:String ) {
			jsonString = s;
			loc = 0;
			nextChar();
			return;
		}
		
		public function getNextToken():JSONToken {
			var token:JSONToken = new JSONToken();
			skipIgnored();
			
			switch ( ch ) {
				
				case '{':
					token.type = JSONTokenType.LEFT_BRACE;
					token.value = '{';
					nextChar();
					break
				
				case '}':
					token.type = JSONTokenType.RIGHT_BRACE;
					token.value = '}';
					nextChar();
					break
				
				case '[':
					token.type = JSONTokenType.LEFT_BRACKET;
					token.value = '[';
					nextChar();
					break
				
				case ']':
					token.type = JSONTokenType.RIGHT_BRACKET;
					token.value = ']';
					nextChar();
					break
				
				case ',':
					token.type = JSONTokenType.COMMA;
					token.value = ',';
					nextChar();
					break
				
				case ':':
					token.type = JSONTokenType.COLON;
					token.value = ':';
					nextChar();
					break;
				
				case 't':
					var possibleTrue:String = "t" + nextChar() + nextChar() + nextChar();
					
					if ( possibleTrue == "true" ) {
						token.type = JSONTokenType.TRUE;
						token.value = true;
						nextChar();
					} else {
						parseError( "Expecting 'true' but found " + possibleTrue );
					}
					
					break;
				
				case 'f': 
					var possibleFalse:String = "f" + nextChar() + nextChar() + nextChar() + nextChar();
					
					if ( possibleFalse == "false" ) {
						token.type = JSONTokenType.FALSE;
						token.value = false;
						nextChar();
					} else {
						parseError( "Expecting 'false' but found " + possibleFalse );
					}
					
					break;
				
				case 'n': 
					
					var possibleNull:String = "n" + nextChar() + nextChar() + nextChar();
					
					if ( possibleNull == "null" ) {
						token.type = JSONTokenType.NULL;
						token.value = null;
						nextChar();
					} else {
						parseError( "Expecting 'null' but found " + possibleNull );
					}
					
					break;
				
				case '"': 
					token = readString();
					break;
				
				default: 
					if ( isDigit( ch ) || ch == '-' ) {
						token = readNumber();
					} else if ( ch == '' ) {
						return null;
					} else {                                                
						parseError( "Unexpected " + ch + " encountered" );
					}
			}
			
			return token;
		}
		
		private function readString():JSONToken {
			var token:JSONToken = new JSONToken();
			token.type = JSONTokenType.STRING;
			
			var string:String = "";
			
			nextChar();
			
			while ( ch != '"' && ch != '' ) {
				
				if ( ch == '\\' ) {
					
					nextChar();
					
					switch ( ch ) {
						
						case '"': // quotation mark
							string += '"';
							break;
						
						case '/':       // solidus
							string += "/";
							break;
						
						case '\\':      // reverse solidus
							string += '\\';
							break;
						
						case 'b':       // bell
							string += '\b';
							break;
						
						case 'f':       // form feed
							string += '\f';
							break;
						
						case 'n':       // newline
							string += '\n';
							break;
						
						case 'r':       // carriage return
							string += '\r';
							break;
						
						case 't':       // horizontal tab
							string += '\t'
							break;
						
						case 'u':
							var hexValue:String = "";
							
							for ( var i:int = 0; i < 4; i++ ) {
								if ( !isHexDigit( nextChar() ) ) {
									parseError( " Excepted a hex digit, but found: " + ch );
								}
								// valid, add it to the value
								hexValue += ch;
							}
							
							string += String.fromCharCode( parseInt( hexValue, 16 ) );
							
							break;
						
						default:
							string += '\\' + ch;
							
					}
					
				} else {
					string += ch;
					
				}
				
				nextChar();
				
			}
			if ( ch == '' ) {
				parseError( "Unterminated string literal" );
			}
			
			// move past the closing " in the input string
			nextChar();
			
			// attach to the string to the token so we can return it
			token.value = string;
			
			return token;
		}
		
		private function readNumber():JSONToken {
			var token:JSONToken = new JSONToken();
			token.type = JSONTokenType.NUMBER;
			
			var input:String = "";
			
			// check for a negative number
			if ( ch == '-' ) {
				input += '-';
				nextChar();
			}
			
			// the number must start with a digit
			if ( !isDigit( ch ) )
			{
				parseError( "Expecting a digit" );
			}
			
			// 0 can only be the first digit if it
			// is followed by a decimal point
			if ( ch == '0' )
			{
				input += ch;
				nextChar();
				
				// make sure no other digits come after 0
				if ( isDigit( ch ) )
				{
					parseError( "A digit cannot immediately follow 0" );
				}
			}
			else
			{
				// read numbers while we can
				while ( isDigit( ch ) ) {
					input += ch;
					nextChar();
				}
			}
			
			// check for a decimal value
			if ( ch == '.' ) {
				input += '.';
				nextChar();
				
				// after the decimal there has to be a digit
				if ( !isDigit( ch ) )
				{
					parseError( "Expecting a digit" );
				}
				
				// read more numbers to get the decimal value
				while ( isDigit( ch ) ) {
					input += ch;
					nextChar();
				}
			}
			
			// check for scientific notation
			if ( ch == 'e' || ch == 'E' )
			{
				input += "e"
				nextChar();
				// check for sign
				if ( ch == '+' || ch == '-' )
				{
					input += ch;
					nextChar();
				}
				
				// require at least one number for the exponent
				// in this case
				if ( !isDigit( ch ) )
				{
					parseError( "Scientific notation number needs exponent value" );
				}
				
				// read in the exponent
				while ( isDigit( ch ) )
				{
					input += ch;
					nextChar();
				}
			}
			
			// convert the string to a number value
			var num:Number = Number( input );
			
			if ( isFinite( num ) && !isNaN( num ) ) {
				token.value = num;
				return token;
			} else {
				parseError( "Number " + num + " is not valid!" );
			}
			return null;
		}
		
		private function nextChar():String {
			return ch = jsonString.charAt( loc++ );
		}
		
		private function skipIgnored():void
		{
			var originalLoc:int;
			
			// keep trying to skip whitespace and comments as long
			// as we keep advancing past the original location 
			do
			{
				originalLoc = loc;
				skipWhite();
				skipComments();
			}
			while ( originalLoc != loc );
		}
		
		private function skipComments():void {
			if ( ch == '/' ) {
				// Advance past the first / to find out what type of comment
				nextChar();
				switch ( ch ) {
					case '/': // single-line comment, read through end of line
						
						// Loop over the characters until we find
						// a newline or until there's no more characters left
						do {
							nextChar();
						} while ( ch != '\n' && ch != '' )
						
						// move past the \n
						nextChar();
						
						break;
					
					case '*': // multi-line comment, read until closing */
						
						// move past the opening *
						nextChar();
						
						// try to find a trailing */
						while ( true ) {
							if ( ch == '*' ) {
								// check to see if we have a closing /
								nextChar();
								if ( ch == '/') {
									// move past the end of the closing */
									nextChar();
									break;
								}
							} else {
								// move along, looking if the next character is a *
								nextChar();
							}
							
							// when we're here we've read past the end of 
							// the string without finding a closing */, so error
							if ( ch == '' ) {
								parseError( "Multi-line comment not closed" );
							}
						}
						
						break;
					
					// Can't match a comment after a /, so it's a parsing error
					default:
						parseError( "Unexpected " + ch + " encountered (expecting '/' or '*' )" );
				}
			}
			
		}
		
		
		private function skipWhite():void {
			
			// As long as there are spaces in the input 
			// stream, advance the current location pointer
			// past them
			while ( isWhiteSpace( ch ) ) {
				nextChar();
			}
			
		}
		
		private function isWhiteSpace( ch:String ):Boolean {
			return ( ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' );
		}
		
		private function isDigit( ch:String ):Boolean {
			return ( ch >= '0' && ch <= '9' );
		}
		
		private function isHexDigit( ch:String ):Boolean {
			// get the uppercase value of ch so we only have
			// to compare the value between 'A' and 'F'
			var uc:String = ch.toUpperCase();
			
			// a hex digit is a digit of A-F, inclusive ( using
			// our uppercase constraint )
			return ( isDigit( ch ) || ( uc >= 'A' && uc <= 'F' ) );
		}
		
		public function parseError( message:String ):void {
			throw new JSONParseError( message, loc, jsonString );
		}

	}
}